{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d2b29a4b",
   "metadata": {},
   "source": [
    "# 멀티 에이전트 협업 네트워크\n",
    "\n",
    "이 튜토리얼에서는 **멀티 에이전트 네트워크**를 LangGraph를 활용하여 구현하는 방법을 다룹니다.  \n",
    "멀티 에이전트 네트워크는 복잡한 작업을 여러 개의 전문화된 에이전트들로 나누어 처리하는 \"분할 정복\" 접근 방식을 사용하는 아키텍처입니다. \n",
    "\n",
    "이를 통해 단일 에이전트가 많은 도구를 비효율적으로 사용하는 문제를 해결하고, 각 에이전트가 자신의 전문 분야에서 효과적으로 문제를 해결하도록 합니다.\n",
    "\n",
    "본 튜토리얼은 [AutoGen 논문](https://arxiv.org/abs/2308.08155)에서 영감을 받아, LangGraph를 활용하여 이러한 멀티 에이전트 네트워크를 구축하는 방법을 단계별로 살펴봅니다. 또한, LangSmith를 사용하여 프로젝트 성능을 개선하고 문제를 빠르게 식별하는 방법도 소개합니다.\n",
    "\n",
    "![](./assets/langgraph-multi-agent.png)\n",
    "\n",
    "---\n",
    "\n",
    "**왜 멀티 에이전트 네트워크인가?**\n",
    "\n",
    "단일 에이전트는 특정 도메인 내에서 일정 수의 도구를 사용할 때 효율적일 수 있습니다. 그러나 한 에이전트가 너무 많은 도구를 다루면,  \n",
    "1. 도구 사용 로직이 복잡해지고,  \n",
    "2. 에이전트가 한 번에 처리해야 할 정보 양이 증가하여 비효율적일 수 있습니다.\n",
    "\n",
    "\"분할 정복\" 접근을 사용하면 각 에이전트는 특정 업무나 전문성 영역에 집중할 수 있고, 전체 작업이 네트워크 형태로 나뉘어 처리됩니다.  \n",
    "각 에이전트는 자신이 잘하는 일을 처리하고, 필요 시 해당 업무를 다른 전문 에이전트에게 위임하거나 도구를 적절히 활용합니다.\n",
    "\n",
    "---\n",
    "\n",
    "**주요 내용**\n",
    "\n",
    "- **에이전트 생성**: 에이전트를 정의하고, 이를 LangGraph 그래프의 노드로 설정하는 방법  \n",
    "- **도구 정의**: 에이전트가 사용할 도구를 정의하고 노드로 추가하는 방법  \n",
    "- **그래프 생성**: 에이전트와 도구를 연결하여 멀티 에이전트 네트워크 그래프를 구성하는 방법  \n",
    "- **상태 정의**: 그래프 상태를 정의하고, 각 에이전트의 동작에 필요한 상태 정보를 관리하는 방법  \n",
    "- **에이전트 노드 정의**: 각각의 전문 에이전트를 노드로 정의하는 방법  \n",
    "- **도구 노드 정의**: 도구를 노드로 정의하고 에이전트가 이 도구를 활용하도록 하는 방법  \n",
    "- **엣지 로직 정의**: 에이전트 결과에 따라 다른 에이전트나 도구로 분기하는 로직을 설정하는 방법  \n",
    "- **그래프 정의**: 위에서 정의한 에이전트, 도구, 상태, 엣지 로직을 종합하여 최종 그래프를 구성하는 방법  \n",
    "- **그래프 실행**: 구성된 그래프를 호출하고 실제 작업을 수행하는 방법\n",
    "\n",
    "---\n",
    "\n",
    "**참고**\n",
    "\n",
    "이 튜토리얼에 제시되는 패턴은 LangGraph에서 복잡한 에이전트 네트워크를 구성하기 위한 특정 디자인 패턴을 보여주는 예시입니다.  \n",
    "실제 적용 상황에 따라 이 패턴들을 수정하거나, LangGraph 문서에서 제안하는 다른 기본 패턴과 결합하여 최적의 성능을 도출할 것을 권장합니다.\n",
    "\n",
    "**주요 참고 자료**  \n",
    "- [LangGraph 멀티 에이전트 네트워크 개념](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#network)  \n",
    "- [AutoGen 논문: Enabling Next-Gen LLM Applications via Multi-Agent Conversation (Wu et al.)](https://arxiv.org/abs/2308.08155)  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d3a2e7e",
   "metadata": {},
   "source": [
    "## 환경 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7862a931",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e2785cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Use-Cases\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f756407",
   "metadata": {},
   "source": [
    "이번 에이전트에 사용할 모델명을 지정합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3aa2e6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.models import get_model_name, LLMs\n",
    "\n",
    "# 최신 모델 이름 가져오기\n",
    "MODEL_NAME = get_model_name(LLMs.GPT4o)\n",
    "\n",
    "print(MODEL_NAME)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99e92c04",
   "metadata": {},
   "source": [
    "## 상태 정의\n",
    "\n",
    "`messages` 는 Agent 간 공유하는 메시지 목록이며, `sender` 는 마지막 메시지의 발신자입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5abc0450",
   "metadata": {},
   "outputs": [],
   "source": [
    "import operator\n",
    "from typing import Annotated, Sequence\n",
    "from typing_extensions import TypedDict\n",
    "from langchain_core.messages import BaseMessage\n",
    "\n",
    "\n",
    "# 상태 정의\n",
    "class AgentState(TypedDict):\n",
    "    messages: Annotated[\n",
    "        Sequence[BaseMessage], operator.add\n",
    "    ]  # Agent 간 공유하는 메시지 목록\n",
    "    sender: Annotated[str, \"The sender of the last message\"]  # 마지막 메시지의 발신자"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61eb7e16",
   "metadata": {},
   "source": [
    "## 도구 정의\n",
    "\n",
    "에이전트가 앞으로 사용할 몇 가지 도구를 정의합니다.\n",
    "\n",
    "- `TavilySearch` 는 인터넷에서 정보를 검색하는 도구입니다. `Research Agent` 가 필요한 정보를 검색할 때 사용합니다.\n",
    "- `PythonREPL` 는 Python 코드를 실행하는 도구입니다. `Chart Generator Agent` 가 차트를 생성할 때 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d4d8f6e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated\n",
    "\n",
    "from langchain_teddynote.tools.tavily import TavilySearch\n",
    "from langchain_core.tools import tool\n",
    "from langchain_experimental.utilities import PythonREPL\n",
    "\n",
    "# Tavily 검색 도구 정의\n",
    "tavily_tool = TavilySearch(max_results=5)\n",
    "\n",
    "# Python 코드를 실행하는 도구 정의\n",
    "python_repl = PythonREPL()\n",
    "\n",
    "\n",
    "# Python 코드를 실행하는 도구 정의\n",
    "@tool\n",
    "def python_repl_tool(\n",
    "    code: Annotated[str, \"The python code to execute to generate your chart.\"],\n",
    "):\n",
    "    \"\"\"Use this to execute python code. If you want to see the output of a value,\n",
    "    you should print it out with `print(...)`. This is visible to the user.\"\"\"\n",
    "    try:\n",
    "        # 주어진 코드를 Python REPL에서 실행하고 결과 반환\n",
    "        result = python_repl.run(code)\n",
    "    except BaseException as e:\n",
    "        return f\"Failed to execute code. Error: {repr(e)}\"\n",
    "    # 실행 성공 시 결과와 함께 성공 메시지 반환\n",
    "    result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n",
    "    return (\n",
    "        result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ce2e2dc",
   "metadata": {},
   "source": [
    "## 에이전트 생성\n",
    "\n",
    "### Research Agent\n",
    "\n",
    "`TavilySearch` 도구를 사용하여 연구를 수행하는 에이전트를 생성합니다. 이 에이전트를 필요한 정보를 리서치하는 데 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "400e5479",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_system_prompt(suffix: str) -> str:\n",
    "    return (\n",
    "        \"You are a helpful AI assistant, collaborating with other assistants.\"\n",
    "        \" Use the provided tools to progress towards answering the question.\"\n",
    "        \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n",
    "        \" will help where you left off. Execute what you can to make progress.\"\n",
    "        \" If you or any of the other assistants have the final answer or deliverable,\"\n",
    "        \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n",
    "        f\"\\n{suffix}\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "89c72d9b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langgraph.graph import MessagesState\n",
    "\n",
    "# LLM 정의\n",
    "llm = ChatOpenAI(model=MODEL_NAME)\n",
    "\n",
    "# Research Agent 생성\n",
    "research_agent = create_react_agent(\n",
    "    llm,\n",
    "    tools=[tavily_tool],\n",
    "    state_modifier=make_system_prompt(\n",
    "        \"You can only do research. You are working with a chart generator colleague.\"\n",
    "    ),\n",
    ")\n",
    "\n",
    "\n",
    "# Research Agent 노드 정의\n",
    "def research_node(state: MessagesState) -> MessagesState:\n",
    "    result = research_agent.invoke(state)\n",
    "\n",
    "    # 마지막 메시지를 HumanMessage 로 변환\n",
    "    last_message = HumanMessage(\n",
    "        content=result[\"messages\"][-1].content, name=\"researcher\"\n",
    "    )\n",
    "    return {\n",
    "        # Research Agent 의 메시지 목록 반환\n",
    "        \"messages\": [last_message],\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d9e53f7",
   "metadata": {},
   "source": [
    "### Chart Generator Agent\n",
    "\n",
    "`PythonREPL` 도구를 사용하여 차트를 생성하는 에이전트를 생성합니다. 이 에이전트를 차트를 생성하는 데 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f7cf3971",
   "metadata": {},
   "outputs": [],
   "source": [
    "chart_generator_system_prompt = \"\"\"\n",
    "You can only generate charts. You are working with a researcher colleague.\n",
    "Be sure to use the following font code in your code when generating charts.\n",
    "\n",
    "##### 폰트 설정 #####\n",
    "import platform\n",
    "\n",
    "# OS 판단\n",
    "current_os = platform.system()\n",
    "\n",
    "if current_os == \"Windows\":\n",
    "    # Windows 환경 폰트 설정\n",
    "    font_path = \"C:/Windows/Fonts/malgun.ttf\"  # 맑은 고딕 폰트 경로\n",
    "    fontprop = fm.FontProperties(fname=font_path, size=12)\n",
    "    plt.rc(\"font\", family=fontprop.get_name())\n",
    "elif current_os == \"Darwin\":  # macOS\n",
    "    # Mac 환경 폰트 설정\n",
    "    plt.rcParams[\"font.family\"] = \"AppleGothic\"\n",
    "else:  # Linux 등 기타 OS\n",
    "    # 기본 한글 폰트 설정 시도\n",
    "    try:\n",
    "        plt.rcParams[\"font.family\"] = \"NanumGothic\"\n",
    "    except:\n",
    "        print(\"한글 폰트를 찾을 수 없습니다. 시스템 기본 폰트를 사용합니다.\")\n",
    "\n",
    "##### 마이너스 폰트 깨짐 방지 #####\n",
    "plt.rcParams[\"axes.unicode_minus\"] = False  # 마이너스 폰트 깨짐 방지\n",
    "\"\"\"\n",
    "\n",
    "# Chart Generator Agent 생성\n",
    "chart_agent = create_react_agent(\n",
    "    llm,\n",
    "    [python_repl_tool],\n",
    "    state_modifier=make_system_prompt(chart_generator_system_prompt),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "4c0ccbe8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def chart_node(state: MessagesState) -> MessagesState:\n",
    "    result = chart_agent.invoke(state)\n",
    "\n",
    "    # 마지막 메시지를 HumanMessage 로 변환\n",
    "    last_message = HumanMessage(\n",
    "        content=result[\"messages\"][-1].content, name=\"chart_generator\"\n",
    "    )\n",
    "    return {\n",
    "        # share internal message history of chart agent with other agents\n",
    "        \"messages\": [last_message],\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "0c1059c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END\n",
    "\n",
    "\n",
    "def router(state: MessagesState):\n",
    "    # This is the router\n",
    "    messages = state[\"messages\"]\n",
    "    last_message = messages[-1]\n",
    "    if \"FINAL ANSWER\" in last_message.content:\n",
    "        # Any agent decided the work is done\n",
    "        return END\n",
    "    return \"continue\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0143a325",
   "metadata": {},
   "source": [
    "## 그래프 생성"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc49d497",
   "metadata": {},
   "source": [
    "### 에이전트 노드 및 엣지 정의\n",
    "\n",
    "이제 노드를 정의해야 합니다. 먼저, 에이전트에 대한 노드를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "01fc6ca0",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import HumanMessage, ToolMessage\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "workflow = StateGraph(MessagesState)\n",
    "workflow.add_node(\"researcher\", research_node)\n",
    "workflow.add_node(\"chart_generator\", chart_node)\n",
    "\n",
    "workflow.add_conditional_edges(\n",
    "    \"researcher\",\n",
    "    router,\n",
    "    {\"continue\": \"chart_generator\", END: END},\n",
    ")\n",
    "workflow.add_conditional_edges(\n",
    "    \"chart_generator\",\n",
    "    router,\n",
    "    {\"continue\": \"researcher\", END: END},\n",
    ")\n",
    "\n",
    "workflow.add_edge(START, \"researcher\")\n",
    "app = workflow.compile(checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "361a3bd1",
   "metadata": {},
   "source": [
    "생성한 그래프 시각화를 진행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2093ac6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app, xray=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27f19ddf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_teddynote.messages import random_uuid, invoke_graph\n",
    "\n",
    "# config 설정(재귀 최대 횟수, thread_id)\n",
    "config = RunnableConfig(recursion_limit=10, configurable={\"thread_id\": random_uuid()})\n",
    "\n",
    "# 질문 입력\n",
    "inputs = {\n",
    "    \"messages\": [\n",
    "        HumanMessage(\n",
    "            content=\"2010년 ~ 2024년까지의 대한민국의 1인당 GDP 추이를 그래프로 시각화 해주세요.\"\n",
    "        )\n",
    "    ],\n",
    "}\n",
    "\n",
    "# 그래프 실행\n",
    "invoke_graph(app, inputs, config, node_names=[\"researcher\", \"chart_generator\", \"agent\"])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
